在先前的文章中,我們花了很多的時間來討論閉包,這是為什麼呢?因為在 FP 中,如果我們想要更有效率、更嚴謹的方法來撰寫函式,了解函式在呼叫堆疊中的運行模式可以說是非常重要的,因為這是我們要使用「科里化」來優化函式的必備知識。
看到這邊的大家,可能會覺得:「咦?不是已經有純函式了嗎?」
難道純函式還不夠嚴謹嗎?
在介紹科里化之前,讓我們來回顧一下上一章節的範例:
const makeName = (name) => `My name is ${name}`;
const newName = makeName('Vivian');
console.log(newName);
// My name is Vivian
在上方的範例中,我們撰寫了一個名字產生器,只要帶入一個參數,就可以產出帶有參數的指定字串,但問題來了,如果我們要帶入不只一個參數呢?如果我們想要傳入多個名字的話:
const makeName = (firstName, secondName) => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian', 'Joe');
console.log(newName);
// First name is Vivian; second name is Joe.
這裡乍看之下好像沒有什麼問題,如果此時有另外一個人想要使用這個函式,但是不小心忘記帶入第二個參數,這時候會發生什麼事呢?
const makeName = (firstName, secondName) => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian');
console.log(newName);
// newName = ?
沒錯,由於我們並沒有帶入第二個參數,所以 secondName
在函式運行時無法被參照,所以成了 undefined
,最後輸出的結果會是:
// First name is Vivian; second name is undefined.
當然也有人會覺得:「啊!那就給定預設值就好了呀?」如果給定預設值的話,我們函式會長成:
const makeName = (firstName, secondName='') => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian');
console.log(newName);
// First name is Vivian; second name is.
這樣的函式顯然美中不足,雖然它已經符合純函式的規範,但我們可能還要額外撰寫一堆判斷式來決定我們的回傳值,也依然沒有解決使用者不一定知道在函式中要帶入幾個參數的問題,而且可想而知地,函式的可複用性又是另一個問題。
這就是前文我們所提到,為什麼 純函式依然有他的問題的存在的原因,但沒有關係,這個問題我們可以很好地透過「科里化」來解決。
其實最初科里化(Currying)是一個數學理論,後續才由 Haskell Curry 發揚光大,故名「Currying」。
科里化的核心概念是將函式要帶入的多個參數,轉換為一次性帶入一個參數,舉例來說:
// 此處函式 f 僅示意某個需要帶入多個函式之參數
const a = f(arg1, arg2, arg3);
// Currying 後
const a = curriedF(arg1);
const b = a(arg2);
const c = b(arg3);
// 也可以進行鏈型串接
const chain = curriedF(arg1)(arg2)(arg3);
這麼做有什麼好處呢?讓我們來看看科里化與純函式相比有什麼好處:
難道上述兩個優點純函式做不到嗎?是的純函式還真的做不到,讓我們直接來看範例比較:
// 如果我們要用純函式固定第一個參數:
const a = f(arg1, arg2, arg3);
const b = f(arg1, arg4, arg5);
// 透過科里化固定參數:
const c = curriedF(arg1);
const d = c(arg2)(arg3);
const e = c(arg4)(arg5);
我們會發現若是透過純函式固定第一個參數,其實沒什麼意義,因為我們還是會不斷重複帶入同樣的參數,光是上方的程式碼,就足以想見未來若是要重複使用函式,未經科里化的 Pure Funciton 程式碼會越來越多。
但經過先前的介紹,我們其實完全可以透過 JavaScript 執行堆疊的特性,降低重複傳入固定參數的次數,因為 JavaScript 本身就自帶保留參數的機制,我們就更應該好好使用。
既然了解科里化的好處,就讓我們把之前的名字產生器做一個更好的優化吧:
const makeName = (firstName) => (secondName) => `First name is ${firstName}; second name is ${secondName}.`;;
const makeFirstName = makeName('Vivian');
console.log(makeFirstName);
// output = ?
我們透過科里化的方式,將函式透過閉包的技巧留住每次傳入參數,當我們固定一個參數時,輸出會長怎麼樣呢?
(secondName) => `First name is ${firstName}; second name is ${secondName}.`
由於我們還沒有帶入第二個參數,所以 makeFirstName
會回傳一個函式,此時讓我們來試試看,究竟第一個韓式有沒有被固定住:
const secondNameIsJoe = makeFirstName('Joe');
const secondNameIsTim = makeFirstName('Tim');
console.log(secondNameIsJoe);
console.log(secondNameIsTim);
// output 1 : 'First name is Vivian; second name is Joe.'
// output 2 : 'First name is Vivian; second name is Tim.'
很棒,第一個參數確實被固定住了,這樣就完成了我們的第一個科里化函式了!是不是比原本的純函式複用性更加高了呢?
當然,為了讓大家能更輕易理解科里化的好處及應用,這邊舉的例子相對簡單,但其實在實務開發中,可以透過這個機制完成更多更複雜的應用。
如果你是第一次接觸柯里化函式,可能會覺得需要花點時間去消化了解,那都是正常的,在下一個章節中,我們將要聊聊另外一個概念「高階函式」,來讓我們更加理解目前為止所學習到的工具,那麼就讓我們下一章節見吧!